--[[ 

 This module provides lusi's simulation inpur parser written
 in lpeg.

 (c) 2013 Markus Dittrich

 This program is free software; you can redistribute it
 and/or modify it under the terms of the GNU General Public
 License Version 3 as published by the Free Software Foundation.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License Version 3 for more details.

 You should have received a copy of the GNU General Public
 License along with this program; if not, write to the Free
 Software Foundation, Inc., 59 Temple Place - Suite 330,
 Boston, MA 02111-1307, USA.

--]]

-- use this only during development
--tostring = require 'ml'.tstring

local lpeg = require "lpeg"
local rpn_engine = require "rpn_engine"
lpeg.locale(lpeg)


-- helper function for debugging
function debug(content)
  print("DEBUG *** ", content)
  return content
end

-- this closure takes care of adding any defined 
-- variables to a global variables list
local variables = {}
function variable_appender_generator(variables)

  return function (varName, varValue)
    variables[varName] = varValue
    return
  end
end

-- this is the appender function passed to the parser 
-- generator. 
-- NOTE: Is there a way to define this locally within
--       parse_input instead of defining it globally
local variable_appender = variable_appender_generator(variables)


-- RPN parser helper functions responsible for arranging
-- the parsed infix expressions into postfix needed for RPN
function addition(list, content)
  table.insert(list, content)
  table.insert(list, "+")
  return list 
end


function subtraction(list, content)
  table.insert(list, content)
  table.insert(list, "-")
  return list 
end


function multiplication(list, content)
  table.insert(list, content)
  table.insert(list, "*")
  return list 
end


function division(list, content)
  table.insert(list, content)
  table.insert(list, "/")
  return list 
end


function exponentiation(list, content)
  table.insert(list, content)
  table.insert(list, "^")
  return list 
end


function function_application(list, content)
  -- the 1 is required to swap the function token to 
  -- the end of the list 
  table.insert(list, 1, content)
  return list
end


-- lexical elements
local C, Cf, Cg, Ct, P = lpeg.C, lpeg.Cf, lpeg.Cg, lpeg.Ct, lpeg.P
local R, S, V      = lpeg.R, lpeg.S, lpeg.V

local Space  = S(" \n\t\r")^0
local MaybeNumber = C(R("09")^0)
local Number  = C(P"-"^-1 * R("09")^1) 

local digit = R('09')
local number_sign = S'+-'^-1
local Float = C((digit^1 * P'.' * digit^0 + P'.' * digit^1) *
               (S'eE' * number_sign * digit^1)^-1)

local Equal = "=" * Space
local Plus = "+" * Space
local Colon = ":" * Space
local OpenBrace  = "{" * Space
local CloseBrace = "}" * Space
local OpenRect = "[" * Space
local OpenParens = "(" * Space
local CloseParens = ")" * Space
local CloseRect = "]" * Space
local Arrow = "->" * Space

local Comment = P("--") * (S" \t" + lpeg.alnum + lpeg.punct)^0 * Space;

local ParamSpec = "Parameters" * Space
local TimeKey = "simulation_time" * Space
local DispIntervalKey = "display_interval" * Space

local MolSpec = "Molecules" * Space
local NamePrim = R("AZ") + R("az")  
local Name = C(NamePrim * (NamePrim + R("09") + S("_-"))^0) * Space 
local FileName = C(S("\"") * lpeg.alnum^1 * "." * lpeg.alnum^1 * S("\"")) 
                 * Space;

local ReactSpec = "Reactions" * Space

local OutputSpec = "Output" * Space
local OutputIntervalKey = "output_interval" * Space
local OutputWriteKey = "write" * Space

-- RPN parser definitions
local PlusOp = Space * "+" * Space
local MinusOp = Space * "-" * Space
local MultOp = Space * "*" * Space
local DivOp = Space * "/" * Space
local ExpOp = Space * "^" * Space
local FuncName = Space * C(P("sin") + P("cos") + P("tan") + 
                           P("exp") + P("ln") + P("log") +
                           P("sqrt")) * Space


-- grammar definition
local ParameterBlock = V"ParameterBlock"
local ParamDefs = V"ParamDefs"
local TimeDef = V"TimeDef"
local TimeVal = V"TimeVal"
local DispIntervalDef = V"DispIntervalDef"
local DispIntervalVal = V"DispIntervalVal"

local MoleculeBlock = V"MoleculeBlock"
local MoleculeDefs = V"MoleculeDefs"
local MoleculeDef = V"MoleculeDef"
local MolName = V"MolName"
local MolCount = V"MolCount"

local ReactionBlock = V"ReactionBlock"
local ReactionDefs = V"ReactionDefs"
local ReactionDef = V"ReactionDef"
local Reactants = V"Reactants"
local Products = V"Products"
local MolMult = V"MolMult"
local Rate = V"Rate"
local RateFloat = V"RateFloat"
local RateEquation = V"RateEquation"

local OutputBlock = V"OutputBlock"
local OutputDefs = V"OutputDefs"
local OutputIntervalDef = V"OutputIntervalDef"
local OutputIntervalVal = V"OutputIntervalVal"
local OutputWriteDef = V"OutputWriteDef"
local OutputFile = V"OutputFile"

local VariableDefs = V"VariableDefs"
local VariableDef = V"VariableDef"

local CommentDef = V"CommentDef"

local RpnParse = V"RpnParse"
local RpnSub = V"RpnSub"
local RpnMult = V"RpnMult"
local RpnDiv = V"RpnDiv"
local RpnExp = V"RpnExp"
local RpnToken = V"RpnToken"
local RpnParens = V"RpnParens"
local RpnFuncs = V"RpnFuncs"
local RpnNum = V"RpnNum"
local RpnVariable = V"RpnVariable"

local Start = V"Start"

grammar = {
  Start,
  Start = Space * Ct((ParameterBlock + MoleculeBlock + ReactionBlock
          + OutputBlock + CommentDef)^1) * Space * -1;

  -- parameter definitions
  ParameterBlock = ParamSpec * OpenBrace * Cg(ParamDefs, "params") 
                   * CloseBrace * Space;
  ParamDefs = Ct((TimeDef + DispIntervalDef + VariableDefs + CommentDef)^1);
  TimeDef = TimeKey * Equal * TimeVal * Space;
  TimeVal = Cg(Float/tonumber, "time");
  DispIntervalDef = DispIntervalKey * Equal * DispIntervalVal * Space;
  DispIntervalVal = Cg(Number/tonumber, "interval");

  -- molecule definitions
  MoleculeBlock = MolSpec * OpenBrace * Cg(MoleculeDefs, "mols") * CloseBrace
                  * Space;
  MoleculeDefs = Ct((MoleculeDef + VariableDefs + CommentDef)^0);
  MoleculeDef = Ct(MolName * Colon * MolCount * Space);
  MolName = Cg(Name, "name");
  MolCount = Cg(Number/tonumber, "count");

  -- reaction definitions
  ReactionBlock = ReactSpec * OpenBrace * Cg(ReactionDefs, "reactions") 
                  * CloseBrace * Space;
  ReactionDefs = Ct((ReactionDef + VariableDefs + CommentDef)^1);
  ReactionDef = Ct(Cg(Reactants, "reactants") * Arrow 
                   * Cg(Products, "products")
                   * OpenRect * Rate * Space * CloseRect);
  Reactants = Ct(MolMult * (Plus * MolMult)^0);
  Products = Ct(MolMult * (Plus * MolMult)^0);
  MolMult = Ct(Cg(MaybeNumber, "mult") * Cg(Name, "name"));
  Rate = Cg(RpnParse, "rate");
 
  -- RPN parser
  RpnParse  = Cf(RpnSub * (PlusOp * RpnSub)^0, addition);
  RpnSub  = Cf(RpnMult * (MinusOp * RpnMult)^0, subtraction);
  RpnMult = Cf(RpnDiv * (MultOp * RpnDiv)^0, multiplication);
  RpnDiv  = Cf(RpnExp * (DivOp * RpnExp)^0, division);
  RpnExp  = Cf(RpnToken * (ExpOp * RpnToken)^-1, exponentiation);
  RpnToken = RpnParens + RpnNum + RpnFuncs + RpnVariable; 
  RpnFuncs = Cf(Ct(FuncName) * RpnParens, function_application);
  RpnVariable = Ct(Name);
  RpnParens = OpenParens * RpnParse * Space * CloseParens; 
  RpnNum  = Ct(Float + Number);

  -- output definitions
  OutputBlock = OutputSpec * OpenBrace * Cg(OutputDefs, "output") 
                * CloseBrace * Space;
  OutputDefs = Ct((OutputIntervalDef + OutputWriteDef + VariableDefs +
                   CommentDef)^0);
  OutputIntervalDef = OutputIntervalKey * Equal * OutputIntervalVal 
                      * Space;
  OutputIntervalVal = Cg(Number/tonumber, "interval");
  OutputWriteDef = Ct(OutputWriteKey * Space * MolName * Colon * OutputFile);
  OutputFile = Cg(FileName, "output");

  -- comments and variables 
  CommentDef = Comment; 
  VariableDefs = (Name * Equal * RpnParse * Space)/variable_appender;
}


-- function parsing the config file and preparing the
-- data structures
function parse_input(string)

--[[
  for k, val in pairs(grammar) do
    local enter = lpeg.Cmt(lpeg.P(true), function(s, p, ...)
      print("ENTER", k) return p end);
    local leave = lpeg.Cmt(lpeg.P(true), function(s, p, ...)
      print("LEAVE", k) return p end) * (lpeg.P("k") - lpeg.P("k"));
    grammar[k] = lpeg.Cmt(enter * val + leave, function(s, p, ...)
    print("---", k, "---") print(p, s:sub(1, p-1)) return p end)
  end
]]--

  local t = assert(lpeg.match(P(grammar), string), 
                   "*** Error parsing input file ***")

  -- evaluate variables consiting of mathematical expressions
  -- NOTE: We need to flatten everything first since variables could
  --       depend on each other
  for name, value in pairs(variables) do
    variables[name] = rpn_engine.flatten(value, {})
  end

  local computedVars = {}
  for name, value in pairs(variables) do
    computedVars[name] = {rpn_engine.compute_rpn(value, variables, {})}
  end

  -- prepare molecule data structures
  local molecules = {}
  for _, mol in ipairs(t.mols) do
    molecules[mol.name] = mol
  end

  -- prepare reactions
  local reactions = {}
  for _,reacts in ipairs(t.reactions) do

    local reaction = {}

    -- deal with rates. These are parsed as RPN expressions
    -- check if we can evaluate each expression fully (possible if
    -- it doesn't contain variables). Otherwise we leave the RPN
    -- expressions and evaluate them at runtime
    local rateExpression = rpn_engine.flatten(reacts.rate, {})
    local evaluate = rpn_engine.compute_rpn(rateExpression, computedVars, {})
    if evaluate ~= nil then
      reaction.rate = evaluate
    else
      reaction.rate = rateExpression
    end

    -- reactants
    local reactants = {}
    for _,r in ipairs(reacts.reactants) do
      local mult = r.mult 
      if (mult == "") then mult = 1 end

      reactants[#reactants+1] = { mol = molecules[r.name],
                                  mult = tonumber(mult) }
    end
    reaction.reacts = reactants

    -- products
    local products = {}
    for _,r in ipairs(reacts.products) do

      -- no products, we're done
      if r.name == "nil" then
        break
      end

      local mult = r.mult 
      if (mult == "") then mult = 1 end
        
      products[#products+1] = { mol = molecules[r.name],
                                mult = tonumber(mult) }
    end

    reaction.products = products
    reactions[#reactions+1] = reaction
  end

  return molecules, reactions, t.params, computedVars
end



input_parser = {
    parse_input = parse_input 
}

return input_parser
